package org.dreamwork.tools;

import org.dreamwork.tools.concurrent.Message;
import org.dreamwork.tools.concurrent.SimpleBroadcaster;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created by seth.yang on 2018/12/10
 */
public class UpdateManager {
    private static final Logger logger = LogFactory.getLogger (UpdateManager.class);
    private static SimpleBroadcaster broadcaster = SimpleBroadcaster.getInstance ();

    private static UpdateProcessor processor = null;

    public static final String BC_CAT_UPDATE    = "org.dreamwork.update-service.UPDATE_SERVICE";
    public static final int CODE_COMPLETE       = 0;        // 结束
    public static final int CODE_DOWNLOADING    = 1;        // 正在下载
    public static final int CODE_PROGRESS       = 2;        // 更新进度
    /** 错误广播代码 */
    public static final int CODE_EXCEPTION      = 0xff;

    private static final String[] units = {"Bytes/s", "KB/s", "MB/s", "GB/s"};
    private static final DecimalFormat df = new DecimalFormat ("#,###.00");

    /*
     * 广播监听器.
     *
     * 仅关心 complete 和 exception 消息. 当接收到 complete 或 exception 消息时，设置
     * processor 为 null，并且将自身从广播监听器队列里删除
     */
/*
    private static IReceiver receiver = message -> {
        if (message.what == CODE_COMPLETE ||
            message.what == CODE_EXCEPTION) {
            clearProcessor ();
        }
    };
*/

    /**
     * 是否已经有任务在后台执行了
     *
     * @return true if the task is running
     */
    public synchronized static boolean isTaskRunning () {
        return processor != null;
    }

    /**
     * 启动后台更新任务
     */
//    public synchronized static void startUpdateTask (String category, String version, String url, String base) {
    public synchronized static void startUpdateTask (UpdateInfo info, String base) {
        if (processor == null) {
            // 从服务器检查是否有更新版本
//            UpdateInfo info = check (category, version, url);
            if (info != null) {
                // 从服务器获取到更新明细信息，开始下载相关资源

                // 向广播监听器队列里注册监听器
//                broadcaster.register (BC_CAT_UPDATE, receiver);
                processor = new UpdateProcessor (base, info);
                // 启动处理任务
                LocalUtil.service.execute (processor::perform);
//                Looper.invokeLater (processor::perform);
            }
        }
    }

    /**
     * 取消正在运行的任务
     */
    public synchronized static void cancel () {
        if (processor != null) {
            processor.cancel ();
        }
    }

    public static UpdateInfo check (String category, String version, String url) {
        if (LocalUtil.isEmpty (category) || LocalUtil.isEmpty (version) || LocalUtil.isEmpty (url)) {
            logger.log (Level.WARNING, "can't update");
            return null;
        }

        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL (url).openConnection ();
            conn.setDoInput (true);
            conn.setDoOutput (true);
            conn.addRequestProperty ("remote.update.category", category);
            conn.addRequestProperty ("remote.update.version", version);
            conn.connect ();

            int code = conn.getResponseCode ();
            if (code == 200) {
                try (InputStream in = conn.getInputStream ()) {
                    ObjectInputStream ois = new ObjectInputStream (in);
                    try {
                        return (UpdateInfo) ois.readObject ();
                    } catch (Exception ex) {
                        logger.log (Level.SEVERE, ex.getMessage (), ex);
                    }
                }
            }
        } catch (IOException ex) {
            logger.log (Level.WARNING, ex.getMessage (), ex);
            raiseException (ex);
        } finally {
            if (conn != null) {
                conn.disconnect ();
            }
        }

        return null;
    }

    public static List<UpdateInfo.UpdateItem> getScripts () {
        return processor == null ? null : new ArrayList<> (processor.scripts);
    }

    public static void removeScripts () {
        if (processor != null) {
            processor.scripts.clear ();
        }
    }

    private static boolean isEmpty (String text) {
        return text == null || text.trim ().length () == 0;
    }

    private static String formatSpeed (double raw) {
        int index = 0;
        while (raw > 1024) {
            raw /= 1024;
            index ++;
        }

        return df.format (raw) + ' ' + units [index];
    }

    private static class UpdateProcessor {
        private UpdateInfo info;
        private String base;

        private List<UpdateInfo.UpdateItem> scripts = new ArrayList<> ();

        UpdateProcessor (String base, UpdateInfo info) {
            this.base = base;
            this.info = info;
        }

        void perform () {
            if (info == null || info.items == null || info.items.length == 0) {
                return;
            }

            // 广播进度：开始下载资源
            int count = 0;
            for (UpdateInfo.UpdateItem item : info.items) {
                if (!isEmpty (item.action)) {
                    if (!"remove".equals (item.action)) {
                        count ++;
                    }
                }
            }

            if (count > 0) {
                // preparing data
                Message message = new Message (CODE_DOWNLOADING);
                message.set ("count", count);
                message.set ("action", "downloading");
                broadcaster.broadcast (BC_CAT_UPDATE, message);

                // download files
                int index = 0;
                String uuid = UUID.randomUUID ().toString ();
                List<File> files = new ArrayList<> ();
                File dir = new File (System.getProperty ("java.io.tmpdir"), "simulator." + uuid);
                if (!dir.exists () && !dir.mkdirs ()) {
                    logger.warning ("Can't create temp dir: " + dir);
                    return;
                }
                dir.deleteOnExit ();
                for (UpdateInfo.UpdateItem item : info.items) {
                    if (!"remove".equals (item.action)) {
                        message.set ("index", ++ index);
                        broadcaster.broadcast (BC_CAT_UPDATE, message);
                        File file = download (dir, item, message);
                        if (file != null) {
                            files.add (file);
                            try {
                                item.downloadedFile = file.getCanonicalPath ();
                            } catch (IOException ex) {
                                ex.printStackTrace ();
                                logger.log (Level.SEVERE, ex, String::new);
                            }
                        }
                    }
                }
                // download completed. kill the target application
                ApplicationShell.kill ();

                // download completed. processing the files
                index = 0;
                message.what = CODE_PROGRESS;
                message.remove ("uri");
                message.set ("count", files.size ());
                message.set ("action", "processing");

                for (UpdateInfo.UpdateItem item : info.items) {
                    message.set ("index", ++ index);
                    message.set ("name", item.name);
                    broadcaster.broadcast (BC_CAT_UPDATE, message);

                    if ("remove".equals (item.action)) {
                        File file = new File (item.location);
                        if (!file.delete ()) {
                            try {
                                logger.warning ("can't remove file: " + file.getCanonicalPath ());
                            } catch (IOException e) {
                                e.printStackTrace ();
                            }
                        }
                    } else if ("execute".equals (item.action)) {
                        scripts.add (item);
                    } else if ("update".equals (item.action)) {
                        File file = new File (item.location);
                        if (logger.isLoggable (Level.FINE)) {
                            logger.fine ("trying to delete file: " + file.toPath ().toAbsolutePath ());
                        }
                        if (file.delete ()) {
                            File parent = file.getParentFile ();
                            File target = new File (parent, item.name);
                            File source = new File (item.downloadedFile);
                            try {
                                Files.move (source.toPath (), target.toPath ());
                            } catch (IOException e) {
                                e.printStackTrace ();
                            }

                            if (logger.isLoggable (Level.FINE)) {
                                logger.fine (
                                        item.downloadedFile + " move to " +
                                        target.toPath ().toAbsolutePath () + " success"
                                );
                            }
                        } else {
                            if (logger.isLoggable (Level.WARNING)) {
                                logger.warning ("delete file " + file.toPath ().toAbsolutePath () + " fail");
                            }
                        }
                    }
                }
            }
            broadcaster.broadcast (BC_CAT_UPDATE, CODE_COMPLETE);
        }

        void cancel () {

        }

        private File download (File dir, UpdateInfo.UpdateItem item, Message message) {
            HttpURLConnection conn = null;
            try {
                item.uri = item.uri.trim ();
                if (item.uri.charAt (0) == '/') {
                    item.uri = item.uri.substring (1);
                }
                String url = base + '/' + item.uri;

                String name;
                int pos = item.uri.lastIndexOf ('/');
                if (pos > 0) {
                    name = item.uri.substring (pos + 1);
                } else {
                    name = item.uri;
                }

                message.set ("name", name);
                conn = (HttpURLConnection) new URL (url).openConnection ();
                conn.setDoInput (true);
                try (InputStream in = conn.getInputStream ()) {
                    File file = new File (dir, item.name);
                    file.deleteOnExit ();
                    long contentLength = conn.getContentLengthLong ();
                    message.set ("content-length", contentLength);

                    try (OutputStream out = new FileOutputStream (file)) {
                        byte[] buff = new byte [1 << 20];   // 1 MB buffer
                        int length;
                        double speed;
                        long size = 0, start = System.currentTimeMillis (), last = start, now;

                        while ((length = in.read (buff)) != -1) {
                            out.write (buff, 0, length);
                            out.flush ();
                            size += length;
                            now = System.currentTimeMillis ();
                            // 每 500 毫秒更新一次状态
                            if (now - last > 500) {   // 500 ms
                                speed = 1000.0 * size / (now - start);
                                message.set ("speed", formatSpeed (speed));
                                message.set ("size", size);
                                broadcaster.broadcast (BC_CAT_UPDATE, message);
                                last = now;
                            }
                        }
                        // the file download success
                        now = System.currentTimeMillis ();
                        long delta = now - start;
                        if (delta > 0) {
                            speed = 1000.0 * size / (now - start);
                        } else {
                            speed = size;
                        }
                        message.set ("speed", formatSpeed (speed));
                        message.set ("size", size);
                        broadcaster.broadcast (BC_CAT_UPDATE, message);
                    }
                    if (logger.isLoggable (Level.FINE)) {
                        logger.fine ("download complete. -- " + file.getCanonicalPath ());
                    }
                    return file;
                }
            } catch (Exception ex) {
                logger.log (Level.WARNING, ex.getMessage (), ex);
                raiseException (ex);
            } finally {
                if (conn != null) {
                    conn.disconnect ();
                }
            }
            return null;
        }
    }

    private static void raiseException (Exception ex) {
        broadcaster.broadcast (BC_CAT_UPDATE, CODE_EXCEPTION, ex);
    }

    /*
     * 清除处理器，且从广播监听器队列中删除监听器
     */
/*
    private synchronized static void clearProcessor () {
        processor = null;
        broadcaster.unregister (BC_CAT_UPDATE, receiver);
    }
*/
}
